import 'dart:convert';
import 'dart:typed_data';

import '../sqliteasy_core.dart';

class TypeAdapterFactory {
  static Map<String, dynamic> _typeAdapterMap = {
    "int": IntAdapter(),
    "double": DoubleAdapter(),
    "bool": BoolAdapter(),
    "Uint8List":  BytesAdapter(),
    "String": StringAdapter(),
    "DateTime": DateTimeAdapter()
  };

  static void register(Type type, TypeAdapter typeAdapter) {
    _typeAdapterMap[getTypeKey(type)] = typeAdapter;
  }

  static void registerGenericType(Type parentType, Type genericType, TypeAdapter typeAdapter) {
    _typeAdapterMap[getGenericTypeKey(parentType, genericType)] = typeAdapter;
  }

  static TypeAdapter getAdapterOfType(Type type) {
    return _typeAdapterMap[getTypeKey(type)];
  }

  static TypeAdapter getAdapterByKey(String typeKey) {
    return _typeAdapterMap[typeKey.endsWith('?') ? typeKey.substring(0, typeKey.length - 1) : typeKey];
  }

  static TypeAdapter getAdapterOfGenericType(Type parentType, Type genericType) {
    return _typeAdapterMap[getGenericTypeKey(parentType, genericType)];
  }

  static String getTypeKey(Type type) {
    final typeName = type.toString();
    if (typeName.endsWith('?')) {
      return typeName.substring(0, typeName.length - 1);
    }
    return typeName;
  }

  static String getGenericTypeKey(Type parentType, Type genericType) {
    return "${parentType}<${genericType}>";
  }
}

/// Convert Dart type to sqlite type
abstract class TypeAdapter<T> {
  SQLiteType get sqliteType;

  String serialize(T value);

  T deserialize(dynamic input);
}

class TypeHelper {
  static T deserializeByTypeKey<T>(String typeKey, input) {
    return TypeAdapterFactory.getAdapterByKey(typeKey).deserialize(input);
  }

  static T deserializeByType<T>(Type type, input) {
    return TypeAdapterFactory.getAdapterOfType(type).deserialize(input);
  }
}

class IntAdapter extends TypeAdapter<int> {
  @override
  SQLiteType get sqliteType => SQLiteType.integer;

  @override
  String serialize(int value) {
    return value.toString();
  }

  @override
  int deserialize(input) {
    if (input == null) return null;
    if (input is  int) {
      return input;
    }
    return int.tryParse(input);
  }
}

class DoubleAdapter extends TypeAdapter<double> {
  @override
  SQLiteType get sqliteType => SQLiteType.real;

  @override
  String serialize(double value) {
    return (value ?? 0.0).toString();
  }

  @override
  double deserialize(input) {
    return double.tryParse(input.toString());
  }
}

class StringAdapter extends TypeAdapter<String> {
  @override
  SQLiteType get sqliteType => SQLiteType.text;

  @override
  String serialize(String value) {
    return "'${value?.replaceAll("'", "''")}'";
  }

  @override
  String deserialize(input) {
    if (input == null) return null;
    if (input is! String) {
      return input.toString();
    }
    if (input.startsWith("'") && input.endsWith("'") && input.length > 1) {
      return input.substring(1, input.length - 1).replaceAll("''", "'");
    }
    return input;
  }
}

class BoolAdapter extends TypeAdapter<bool> {
  @override
  SQLiteType get sqliteType => SQLiteType.integer;

  @override
  String serialize(bool value) {
    return value == true ? "1" : "0";
  }

  @override
  bool deserialize(input) {
    if (input == null) return null;
    if (input is bool) {
      return input;
    } else if (input is String) {
      return input != "0";
    } else if (input is int) {
      return input != 0;
    }
    return false;
  }
}

class BytesAdapter extends TypeAdapter<Uint8List> {
  @override
  SQLiteType get sqliteType => SQLiteType.blob;

  @override
  String serialize(Uint8List value) {
    return "X'${value.map((e) => e.toRadixString(16).padLeft(2, "0")).join()}'";
  }

  @override
  Uint8List deserialize(input) {
    if (input == null) return null;
    if (input is Uint8List) {
      return input;
    }
    return utf8.encode(input);
  }
}

class DateTimeAdapter extends TypeAdapter<DateTime> {
  @override
  SQLiteType get sqliteType => SQLiteType.integer;

  @override
  String serialize(DateTime value) {
    return value.millisecondsSinceEpoch.toString();
  }

  @override
  DateTime deserialize(input) {
    if (input == null) return null;
    if (input is DateTime) {
      return input;
    }
    int timestamp = int.tryParse(input);
    if (timestamp == null) {
      return null;
    }
    return DateTime.fromMillisecondsSinceEpoch(timestamp);
  }
}
